Sužinokite, kaip būsimas „JavaScript“ iteratorių pagalbininkų pasiūlymas keičia duomenų apdorojimą, naudojant srauto suliejimą, kuris pašalina tarpinius masyvus ir atveria didžiules našumo galimybes pasitelkiant „tingųjį vertinimą“ (lazy evaluation).
Naujas „JavaScript“ našumo šuolis: išsami iteratorių pagalbininkų srauto suliejimo analizė
Programinės įrangos kūrimo pasaulyje našumo siekis yra nuolatinė kelionė. „JavaScript“ kūrėjams įprastas ir elegantiškas duomenų manipuliavimo būdas yra masyvo metodų, tokių kaip .map(), .filter() ir .reduce(), grandinimas. Šis sklandus API yra skaitomas ir išraiškingas, tačiau jis slepia didelę našumo problemą: tarpinių masyvų kūrimą. Kiekvienas grandinės žingsnis sukuria naują masyvą, sunaudodamas atmintį ir procesoriaus ciklus. Dideliems duomenų rinkiniams tai gali virsti našumo katastrofa.
Būtent čia pasirodo TC39 iteratorių pagalbininkų pasiūlymas – novatoriškas ECMAScript standarto papildymas, pasirengęs iš naujo apibrėžti, kaip apdorojame duomenų rinkinius „JavaScript“. Jo esmė – galinga optimizavimo technika, žinoma kaip srauto suliejimas (arba operacijų suliejimas). Šiame straipsnyje pateikiama išsami šios naujos paradigmos analizė, paaiškinanti, kaip ji veikia, kodėl ji svarbi ir kaip ji leis kūrėjams rašyti efektyvesnį, atmintį tausojantį ir galingesnį kodą.
Tradicinio grandinimo problema: tarpinių masyvų istorija
Norėdami visapusiškai įvertinti iteratorių pagalbininkų naujovę, pirmiausia turime suprasti dabartinio, masyvais pagrįsto požiūrio apribojimus. Panagrinėkime paprastą, kasdienę užduotį: iš skaičių sąrašo norime rasti pirmuosius penkis lyginius skaičius, juos padvigubinti ir surinkti rezultatus.
Įprastas požiūris
Naudojant standartinius masyvo metodus, kodas yra švarus ir intuityvus:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ...]; // Įsivaizduokite labai didelį masyvą
const result = numbers
.filter(n => n % 2 === 0) // 1 žingsnis: filtruoti lyginius skaičius
.map(n => n * 2) // 2 žingsnis: padvigubinti juos
.slice(0, 5); // 3 žingsnis: paimti pirmuosius penkis
Šis kodas yra puikiai skaitomas, tačiau išsiaiškinkime, ką „JavaScript“ variklis daro po gaubtu, ypač jei numbers masyve yra milijonai elementų.
- 1 iteracija (
.filter()): Variklis pereina per visąnumbersmasyvą. Jis sukuria naują tarpinį masyvą atmintyje, pavadinkime jįevenNumbers, kuriame saugomi visi testą praėję skaičiai. Jeinumbersturi milijoną elementų, tai galėtų būti maždaug 500 000 elementų masyvas. - 2 iteracija (
.map()): Dabar variklis pereina per visąevenNumbersmasyvą. Jis sukuria antrą tarpinį masyvą, pavadinkime jįdoubledNumbers, kuriame saugomas atvaizdavimo operacijos rezultatas. Tai dar vienas 500 000 elementų masyvas. - 3 iteracija (
.slice()): Galiausiai variklis sukuria trečią, galutinį masyvą, paimdamas pirmuosius penkis elementus išdoubledNumbers.
Paslėptos išlaidos
Šis procesas atskleidžia kelias kritines našumo problemas:
- Didelis atminties paskirstymas: Sukūrėme du didelius laikinus masyvus, kurie buvo iš karto išmesti. Labai dideliems duomenų rinkiniams tai gali sukelti didelį atminties spaudimą, potencialiai sulėtinti programos veikimą ar net ją sutrikdyti.
- Šiukšlių surinkimo pridėtinės išlaidos: Kuo daugiau laikinų objektų sukuriate, tuo sunkiau šiukšlių surinkėjui juos išvalyti, o tai sukelia pauzes ir našumo strigimus.
- Iššvaistyti skaičiavimai: Kelis kartus perėjome per milijonus elementų. Dar blogiau, mūsų galutinis tikslas buvo gauti tik penkis rezultatus. Tačiau
.filter()ir.map()metodai apdorojo visą duomenų rinkinį, atlikdami milijonus nereikalingų skaičiavimų, kol.slice()didžiąją dalį darbo atmetė.
Tai yra pagrindinė problema, kurią iteratorių pagalbininkai ir srauto suliejimas yra sukurti išspręsti.
Pristatome iteratorių pagalbininkus: nauja duomenų apdorojimo paradigma
Iteratorių pagalbininkų pasiūlymas prideda gerai pažįstamų metodų rinkinį tiesiai į Iterator.prototype. Tai reiškia, kad bet kuris objektas, kuris yra iteratorius (įskaitant generatorius ir metodų, tokių kaip Array.prototype.values(), rezultatus), gauna prieigą prie šių galingų naujų įrankių.
Kai kurie iš pagrindinių metodų:
.map(mapperFn).filter(filterFn).take(limit).drop(limit).flatMap(mapperFn).reduce(reducerFn, initialValue).toArray().forEach(fn).some(fn).every(fn).find(fn)
Perrašykime mūsų ankstesnį pavyzdį naudojant šiuos naujus pagalbininkus:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ...];
const result = numbers.values() // 1. Gauti iteratorių iš masyvo
.filter(n => n % 2 === 0) // 2. Sukurti filtro iteratorių
.map(n => n * 2) // 3. Sukurti atvaizdavimo (map) iteratorių
.take(5) // 4. Sukurti paėmimo (take) iteratorių
.toArray(); // 5. Vykdyti grandinę ir surinkti rezultatus
Iš pirmo žvilgsnio kodas atrodo stebėtinai panašus. Pagrindinis skirtumas yra pradinis taškas – numbers.values() – kuris grąžina iteratorių, o ne patį masyvą, ir galutinė operacija – .toArray() – kuri sunaudoja iteratorių galutiniam rezultatui gauti. Tačiau tikroji magija slypi tame, kas vyksta tarp šių dviejų taškų.
Ši grandinė nesukuria jokių tarpinių masyvų. Vietoj to, ji sukonstruoja naują, sudėtingesnį iteratorių, kuris apgaubia ankstesnįjį. Skaičiavimas yra atidėtas. Iš tikrųjų niekas nevyksta, kol nėra iškviečiamas galutinis metodas, pvz., .toArray() ar .reduce(), kad sunaudotų reikšmes. Šis principas vadinamas tingiuoju vertinimu (lazy evaluation).
Srauto suliejimo magija: vieno elemento apdorojimas vienu metu
Srauto suliejimas yra mechanizmas, kuris daro tingųjį vertinimą tokį efektyvų. Užuot apdorojus visą rinkinį atskirais etapais, jis apdoroja kiekvieną elementą per visą operacijų grandinę individualiai.
Surinkimo linijos analogija
Įsivaizduokite gamyklą. Tradicinis masyvo metodas yra panašus į atskirų patalpų turėjimą kiekvienam etapui:
- 1 patalpa (Filtravimas): Visos žaliavos (visas masyvas) atnešamos. Darbuotojai atfiltruoja netinkamas. Tinkamos sudedamos į didelę talpą (pirmąjį tarpinį masyvą).
- 2 patalpa (Atvaizdavimas): Visa talpa su tinkamomis medžiagomis perkeliama į kitą patalpą. Čia darbuotojai modifikuoja kiekvieną elementą. Modifikuoti elementai sudedami į kitą didelę talpą (antrąjį tarpinį masyvą).
- 3 patalpa (Paėmimas): Antroji talpa perkeliama į galutinę patalpą, kur darbuotojas tiesiog paima pirmuosius penkis elementus iš viršaus, o likusius išmeta.
Šis procesas yra eikvojantis transportavimo (atminties paskirstymo) ir darbo (skaičiavimų) požiūriu.
Srauto suliejimas, paremtas iteratorių pagalbininkais, yra tarsi moderni surinkimo linija:
- Vienas konvejeris eina per visas stotis.
- Elementas dedamas ant konvejerio. Jis keliauja į filtravimo stotį. Jei neatitinka reikalavimų, jis pašalinamas. Jei atitinka, jis keliauja toliau.
- Jis nedelsiant keliauja į atvaizdavimo stotį, kur yra modifikuojamas.
- Tada jis keliauja į skaičiavimo stotį (take). Prižiūrėtojas jį suskaičiuoja.
- Tai tęsiasi, po vieną elementą, kol prižiūrėtojas suskaičiuoja penkis sėkmingus elementus. Tuo metu prižiūrėtojas sušunka „STOP!“ ir visa surinkimo linija sustoja.
Šiame modelyje nėra didelių talpų su tarpiniais produktais, o linija sustoja iškart, kai darbas atliktas. Būtent taip veikia iteratorių pagalbininkų srauto suliejimas.
Žingsnis po žingsnio analizė
Pažvelkime į mūsų iteratoriaus pavyzdžio vykdymą: numbers.values().filter(...).map(...).take(5).toArray().
- Iškviečiamas
.toArray(). Jam reikia reikšmės. Jis prašo savo šaltinio,take(5)iteratoriaus, pirmojo elemento. take(5)iteratoriui reikia elemento, kurį galėtų suskaičiuoti. Jis prašo savo šaltinio,mapiteratoriaus, elemento.mapiteratoriui reikia elemento, kurį galėtų transformuoti. Jis prašo savo šaltinio,filteriteratoriaus, elemento.filteriteratoriui reikia elemento, kurį galėtų patikrinti. Jis paima pirmąją reikšmę iš pradinio masyvo iteratoriaus:1.- Reikšmės '1' kelionė: Filtras patikrina
1 % 2 === 0. Tai yra false. Filtro iteratorius atmeta1ir paima kitą reikšmę iš šaltinio:2. - Reikšmės '2' kelionė:
- Filtras patikrina
2 % 2 === 0. Tai yra true. Jis perduoda2įmapiteratorių. mapiteratorius gauna2, apskaičiuoja2 * 2ir perduoda rezultatą,4, įtakeiteratorių.takeiteratorius gauna4. Jis sumažina savo vidinį skaitiklį (nuo 5 iki 4) ir pateikia4.toArray()vartotojui. Pirmasis rezultatas rastas.
- Filtras patikrina
toArray()turi vieną reikšmę. Jis prašotake(5)kitos. Visas procesas kartojasi.- Filtras paima
3(neatitinka), tada4(atitinka).4yra atvaizduojamas į8, kuris yra paimamas. - Tai tęsiasi, kol
take(5)pateikia penkias reikšmes. Penktoji reikšmė bus iš pradinio skaičiaus10, kuris yra atvaizduojamas į20. - Kai tik
take(5)iteratorius pateikia penktąją reikšmę, jis žino, kad jo darbas baigtas. Kitą kartą, kai jo bus paprašyta reikšmės, jis signalizuos, kad baigė. Visa grandinė sustoja. Skaičiai11,12ir milijonai kitų pradiniame masyve net nėra peržiūrimi.
Nauda yra didžiulė: jokių tarpinių masyvų, minimalus atminties naudojimas, o skaičiavimai sustoja kuo anksčiau. Tai yra monumentalus efektyvumo pokytis.
Praktinis taikymas ir našumo privalumai
Iteratorių pagalbininkų galia apima daug daugiau nei paprastą masyvų manipuliavimą. Ji atveria naujas galimybes efektyviai tvarkyti sudėtingas duomenų apdorojimo užduotis.
1 scenarijus: didelių duomenų rinkinių ir srautų apdorojimas
Įsivaizduokite, kad jums reikia apdoroti kelių gigabaitų žurnalo failą arba duomenų srautą iš tinklo lizdo. Įkelti visą failą į masyvą atmintyje dažnai yra neįmanoma.
Naudodami iteratorius (ir ypač asinchroninius iteratorius, kuriuos aptarsime vėliau), galite apdoroti duomenis dalimis.
// Konceptualus pavyzdys su generatoriumi, kuris pateikia eilutes iš didelio failo
function* readLines(filePath) {
// Implementacija, kuri skaito failą eilutė po eilutės, neįkeldama viso failo
// yield line;
}
const errorCount = readLines('huge_app.log').values()
.map(line => JSON.parse(line))
.filter(logEntry => logEntry.level === 'error')
.take(100) // Rasti pirmąsias 100 klaidų
.reduce((count) => count + 1, 0);
Šiame pavyzdyje vienu metu atmintyje laikoma tik viena failo eilutė, kai ji keliauja per konvejerį. Programa gali apdoroti terabaitus duomenų su minimaliu atminties pėdsaku.
2 scenarijus: ankstyvas nutraukimas
Tai jau matėme su .take(), bet tai taip pat taikoma metodams, tokiems kaip .find(), .some() ir .every(). Įsivaizduokite, kad ieškote pirmojo vartotojo didelėje duomenų bazėje, kuris yra administratorius.
Masyvais pagrįstas (neefektyvus):
const firstAdmin = users.filter(u => u.isAdmin)[0];
Čia .filter() pereis per visą users masyvą, net jei pats pirmas vartotojas yra administratorius.
Iteratoriais pagrįstas (efektyvus):
const firstAdmin = users.values().find(u => u.isAdmin);
.find() pagalbininkas tikrins kiekvieną vartotoją po vieną ir nedelsiant sustabdys visą procesą, radęs pirmą atitikmenį.
3 scenarijus: darbas su begalinėmis sekomis
Tingus vertinimas leidžia dirbti su potencialiai begaliniais duomenų šaltiniais, kas yra neįmanoma su masyvais. Generatoriai puikiai tinka tokioms sekoms kurti.
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Rasti pirmuosius 10 Fibonačio skaičių, didesnių nei 1000
const result = fibonacci()
.filter(n => n > 1000)
.take(10)
.toArray();
// rezultatas bus [1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393]
Šis kodas veikia puikiai. fibonacci() generatorius galėtų veikti amžinai, bet kadangi operacijos yra tingios ir .take(10) suteikia sustabdymo sąlygą, programa apskaičiuoja tik tiek Fibonačio skaičių, kiek reikia užklausai patenkinti.
Žvilgsnis į platesnę ekosistemą: asinchroniniai iteratoriai
Šio pasiūlymo grožis yra tas, kad jis taikomas ne tik sinchroniniams iteratoriams. Jis taip pat apibrėžia lygiagretų pagalbininkų rinkinį asinchroniniams iteratoriams AsyncIterator.prototype. Tai yra esminis pokytis šiuolaikiniam „JavaScript“, kur asinchroniniai duomenų srautai yra visur.
Įsivaizduokite puslapiuojamo API apdorojimą, failų srauto skaitymą iš Node.js ar duomenų tvarkymą iš „WebSocket“. Visa tai natūraliai atvaizduojama kaip asinchroniniai srautai. Su asinchroninių iteratorių pagalbininkais galite jiems taikyti tą pačią deklaratyvią .map() ir .filter() sintaksę.
// Konceptualus puslapiuojamo API apdorojimo pavyzdys
async function* fetchAllUsers() {
let url = '/api/users?page=1';
while (url) {
const response = await fetch(url);
const data = await response.json();
for (const user of data.users) {
yield user;
}
url = data.nextPageUrl;
}
}
// Rasti pirmuosius 5 aktyvius vartotojus iš konkrečios šalies
const activeUsers = await fetchAllUsers()
.filter(user => user.isActive)
.filter(user => user.country === 'DE')
.take(5)
.toArray();
Tai suvienija programavimo modelį duomenų apdorojimui „JavaScript“. Nesvarbu, ar jūsų duomenys yra paprastame atmintyje esančiame masyve, ar asinchroniniame sraute iš nuotolinio serverio, galite naudoti tuos pačius galingus, efektyvius ir skaitomus modelius.
Kaip pradėti ir dabartinė būsena
2024 m. pradžioje iteratorių pagalbininkų pasiūlymas yra 3 etape TC39 procese. Tai reiškia, kad dizainas yra baigtas, ir komitetas tikisi, kad jis bus įtrauktas į būsimą ECMAScript standartą. Dabar laukiama jo įgyvendinimo pagrindiniuose „JavaScript“ varikliuose ir atsiliepimų apie šiuos įgyvendinimus.
Kaip naudoti iteratorių pagalbininkus šiandien
- Naršyklių ir Node.js vykdymo aplinkos: Naujausios pagrindinių naršyklių (pvz., „Chrome“/V8) ir Node.js versijos pradeda diegti šias funkcijas. Gali prireikti įjungti specialią vėliavėlę arba naudoti labai naują versiją, kad galėtumėte jas pasiekti natūraliai. Visada patikrinkite naujausias suderinamumo lenteles (pvz., MDN arba caniuse.com).
- Polifilai (Polyfills): Gamybinėms aplinkoms, kurioms reikia palaikyti senesnes vykdymo aplinkas, galite naudoti polifilą. Dažniausias būdas yra per
core-jsbiblioteką, kuri dažnai įtraukiama su transpiliatoriais, tokiais kaip „Babel“. Sukonfigūravę „Babel“ ircore-js, galite rašyti kodą naudodami iteratorių pagalbininkus ir jis bus transformuotas į lygiavertį kodą, veikiantį senesnėse aplinkose.
Išvada: efektyvaus duomenų apdorojimo ateitis „JavaScript“
Iteratorių pagalbininkų pasiūlymas yra daugiau nei tik naujų metodų rinkinys; tai reiškia esminį poslinkį link efektyvesnio, labiau keičiamo mastelio ir išraiškingesnio duomenų apdorojimo „JavaScript“. Priimdamas tingųjį vertinimą ir srauto suliejimą, jis išsprendžia senas našumo problemas, susijusias su masyvo metodų grandinimu dideliuose duomenų rinkiniuose.
Pagrindinės išvados kiekvienam kūrėjui:
- Numatytasis našumas: Iteratorių metodų grandinimas išvengia tarpinių kolekcijų, drastiškai sumažindamas atminties naudojimą ir šiukšlių surinkėjo apkrovą.
- Patobulintas valdymas su tingumu: Skaičiavimai atliekami tik tada, kai reikia, leidžiant ankstyvą nutraukimą ir elegantišką begalinių duomenų šaltinių tvarkymą.
- Vieningas modelis: Tie patys galingi modeliai taikomi tiek sinchroniniams, tiek asinchroniniams duomenims, supaprastinant kodą ir palengvinant sudėtingų duomenų srautų supratimą.
Kai ši funkcija taps standartine „JavaScript“ kalbos dalimi, ji atvers naujus našumo lygius ir leis kūrėjams kurti tvirtesnes ir labiau keičiamo mastelio programas. Atėjo laikas pradėti mąstyti srautais ir pasiruošti rašyti efektyviausią duomenų apdorojimo kodą savo karjeroje.